{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "91910e50-a5ae-4d5a-a431-62ac5fbc11ca",
   "metadata": {},
   "source": [
    "# Joining Spatial Data with Different Coordinate Systems\n",
    "\n",
    "> Note: Before running this notebook, ensure that you have installed SedonaDB: `pip install \"apache-sedona[db]\"`\n",
    "\n",
    "This example demonstrates how one table with an EPSG 4326 CRS cannot be joined with another table that uses EPSG 3857.\n",
    "\n",
    "A Coordinate Reference System (CRS) defines how the two-dimensional coordinates of a map relate to real locations on Earth. Operations like spatial joins, distance calculations, or overlays require all datasets to be in the same CRS to produce accurate results.\n",
    "\n",
    "This notebook demonstrates a key feature of SedonaDB: it protects users from generating incorrect results by raising an error if you attempt to join tables with mismatched coordinate reference systems."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84493c4d",
   "metadata": {},
   "source": [
    "We will walk through two examples:\n",
    "\n",
    "- Joining countries (using EPSG:4326, a geographic CRS) and cities (using EPSG:3857, a projected CRS).\n",
    "\n",
    "- Finding the number of buildings in Vermont by joining two large datasets with different coordinate reference systems."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "be8ffe47-dc89-4842-bb1e-1e8640afffc3",
   "metadata": {},
   "outputs": [],
   "source": [
    "import sedona.db\n",
    "\n",
    "sd = sedona.db.connect()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "54b48173-6be0-4827-ac42-1439eb31e9f7",
   "metadata": {},
   "source": [
    "Read a table with a geometry column that uses EPSG 4326.\n",
    "\n",
    "Note how SedonaDB reads the CRS specified in the Parquet file."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "143f00d5-6878-4dab-a82c-c9fb4dbfaf00",
   "metadata": {},
   "outputs": [],
   "source": [
    "countries = sd.read_parquet(\n",
    "    \"https://raw.githubusercontent.com/geoarrow/geoarrow-data/v0.2.0/natural-earth/files/natural-earth_countries_geo.parquet\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8ef94b7b-b65d-4da5-9443-3253e84e2e7f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "SedonaSchema with 3 fields:\n",
       "  name: utf8<Utf8View>\n",
       "  continent: utf8<Utf8View>\n",
       "  geometry: geometry<WkbView(epsg:4326)>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "countries.schema"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34676ec5",
   "metadata": {},
   "source": [
    "## Example 1: Create a cities table with a Projected CRS\n",
    "\n",
    "We will now create a DataFrame containing several major US cities. The coordinates are provided in Web Mercator (EPSG:3857), which uses meters as its unit.\n",
    "\n",
    "We use the `ST_SetSRID` function to assign the correct CRS identifier to our geometry data. It's important to distinguish between these two key functions:\n",
    "\n",
    "`ST_SetSRID(geometry, srid)`: This function assigns an SRID to a geometry. It does not change the underlying coordinate values. You should only use this when your data has a CRS that SedonaDB was unable to infer.\n",
    "\n",
    "`ST_Transform(geometry, target_srid)`: This function transforms the geometry from its current CRS to a new one. It re-projects the coordinate values themselves."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "579e843f",
   "metadata": {},
   "outputs": [],
   "source": [
    "cities = sd.sql(\"\"\"\n",
    "SELECT city, ST_SetSRID(ST_GeomFromText(wkt), 3857) AS geometry FROM (VALUES\n",
    "    ('New York', 'POINT(-8238310.24 4969803.34)'),\n",
    "    ('Los Angeles', 'POINT(-13153204.78 4037636.04)'),\n",
    "    ('Chicago', 'POINT(-9757148.04 5138517.44)'))\n",
    "AS t(city, wkt)\"\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "36e53438-c2d2-444e-9f34-d391f0f3f588",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "SedonaSchema with 2 fields:\n",
       "  city: utf8<Utf8>\n",
       "  geometry: geometry<Wkb(epsg:3857)>"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "cities.schema"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "62c87571-50aa-4f57-a7dd-4afa3210320a",
   "metadata": {},
   "outputs": [],
   "source": [
    "cities.to_view(\"cities\", overwrite=True)\n",
    "countries.to_view(\"countries\", overwrite=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "561b3c8c-4952-4fa7-9fe1-3fa0522b0d9f",
   "metadata": {},
   "source": [
    "### Join with mismatched Coordinate Reference Systems\n",
    "\n",
    "The cities and countries tables have different CRSs.\n",
    "\n",
    "The cities table uses EPSG:3857 and the countries table uses EPSG:4326.\n",
    "\n",
    "Let's confirm that the code errors out if we try to join the mismatched tables."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "906bad37-4f3f-4028-82b4-487fabe5957f",
   "metadata": {},
   "outputs": [
    {
     "ename": "SedonaError",
     "evalue": "type_coercion\ncaused by\nError during planning: Mismatched CRS arguments: epsg:3857 vs epsg:4326\nUse ST_Transform() or ST_SetSRID() to ensure arguments are compatible.",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mSedonaError\u001b[39m                               Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 6\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;66;03m# join doesn't work when CRSs don't match\u001b[39;00m\n\u001b[32m      2\u001b[39m \u001b[43msd\u001b[49m\u001b[43m.\u001b[49m\u001b[43msql\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\"\"\u001b[39;49m\n\u001b[32m      3\u001b[39m \u001b[33;43mselect * from cities\u001b[39;49m\n\u001b[32m      4\u001b[39m \u001b[33;43mjoin countries\u001b[39;49m\n\u001b[32m      5\u001b[39m \u001b[33;43mwhere ST_Intersects(cities.geometry, countries.geometry)\u001b[39;49m\n\u001b[32m----> \u001b[39m\u001b[32m6\u001b[39m \u001b[33;43m\"\"\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mshow\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n",
      "\u001b[36mFile \u001b[39m\u001b[32m~/new-sedonadb/sedona-db/python/sedonadb/python/sedonadb/dataframe.py:380\u001b[39m, in \u001b[36mDataFrame.show\u001b[39m\u001b[34m(self, limit, width, ascii)\u001b[39m\n\u001b[32m    356\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Print the first limit rows to the console\u001b[39;00m\n\u001b[32m    357\u001b[39m \n\u001b[32m    358\u001b[39m \u001b[33;03mArgs:\u001b[39;00m\n\u001b[32m   (...)\u001b[39m\u001b[32m    377\u001b[39m \n\u001b[32m    378\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m    379\u001b[39m width = \u001b[38;5;28mself\u001b[39m._out_width(width)\n\u001b[32m--> \u001b[39m\u001b[32m380\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_impl\u001b[49m\u001b[43m.\u001b[49m\u001b[43mshow\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_ctx\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlimit\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mwidth\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mascii\u001b[49m\u001b[43m)\u001b[49m, end=\u001b[33m\"\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mSedonaError\u001b[39m: type_coercion\ncaused by\nError during planning: Mismatched CRS arguments: epsg:3857 vs epsg:4326\nUse ST_Transform() or ST_SetSRID() to ensure arguments are compatible."
     ]
    }
   ],
   "source": [
    "# join doesn't work when CRSs don't match\n",
    "sd.sql(\"\"\"\n",
    "select * from cities\n",
    "join countries\n",
    "where ST_Intersects(cities.geometry, countries.geometry)\n",
    "\"\"\").show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41e6f59f-5217-40b2-b05a-9c95eae29df8",
   "metadata": {},
   "source": [
    "### Convert CRS and then join\n",
    "\n",
    "Let's convert the cities table to use EPSG:4326 and then perform the join with the two tables once they have matching CRSs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "122857c1-f68d-4037-9787-54c20706e60f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# update cities to use 4326\n",
    "cities = sd.sql(\"\"\"\n",
    "SELECT city, ST_Transform(geometry, 'EPSG:4326') as geometry\n",
    "FROM cities\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "947e085c-62a4-4315-b155-007a95156964",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "SedonaSchema with 2 fields:\n",
       "  city: utf8<Utf8>\n",
       "  geometry: geometry<Wkb(ogc:crs84)>"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "cities.schema"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "3bcbaf7a-ec40-4b7e-85c2-db5ef1e3232e",
   "metadata": {},
   "outputs": [],
   "source": [
    "cities.to_view(\"cities\", overwrite=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "819c8d04-fa03-4ef8-aecb-e79d48f0b820",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "┌─────────────┬──────────────────────┬──────────────────────┬───────────────┬──────────────────────┐\n",
      "│     city    ┆       geometry       ┆         name         ┆   continent   ┆       geometry       │\n",
      "│     utf8    ┆       geometry       ┆         utf8         ┆      utf8     ┆       geometry       │\n",
      "╞═════════════╪══════════════════════╪══════════════════════╪═══════════════╪══════════════════════╡\n",
      "│ New York    ┆ POINT(-74.006000039… ┆ United States of Am… ┆ North America ┆ MULTIPOLYGON(((-122… │\n",
      "├╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤\n",
      "│ Los Angeles ┆ POINT(-118.15724889… ┆ United States of Am… ┆ North America ┆ MULTIPOLYGON(((-122… │\n",
      "├╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤\n",
      "│ Chicago     ┆ POINT(-87.649952137… ┆ United States of Am… ┆ North America ┆ MULTIPOLYGON(((-122… │\n",
      "└─────────────┴──────────────────────┴──────────────────────┴───────────────┴──────────────────────┘\n"
     ]
    }
   ],
   "source": [
    "# join works when CRSs match\n",
    "sd.sql(\"\"\"\n",
    "select * from cities\n",
    "join countries\n",
    "where ST_Intersects(cities.geometry, countries.geometry)\n",
    "\"\"\").show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5279bebd-1d8d-4f33-bcd9-2c1e93ff7221",
   "metadata": {},
   "source": [
    "## Example #2: Joining two tables with different CRSs\n",
    "\n",
    "This example shows how to join a `vermont` table with an EPSG 32618 CRS with a `buildings` table that uses an EPSG 4326 CRS.\n",
    "\n",
    "The example highlights the following features:\n",
    "\n",
    "1. SedonaDB reads the CRS stored in the files.\n",
    "2. SedonaDB protects you from accidentally joining files with mismatched CRSs.\n",
    "3. It's easy to convert a GeoPandas DataFrame to a SedonaDB DataFrame and maintain the CRS."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "d9ea1469-8e6d-4ef1-a440-5573c1345f0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "import geopandas as gpd\n",
    "\n",
    "path = \"https://raw.githubusercontent.com/geoarrow/geoarrow-data/v0.2.0/example-crs/files/example-crs_vermont-utm.fgb\"\n",
    "gdf = gpd.read_file(path)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "46660482-3fed-4e6c-b37a-2947326e884a",
   "metadata": {},
   "outputs": [],
   "source": [
    "vermont = sd.create_data_frame(gdf)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "1571aa5e-638c-493a-90a1-0cffbeea0bd9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "SedonaSchema with 1 field:\n",
       "  geometry: geometry<Wkb(epsg:32618)>"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "vermont.schema"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "eb4b33f1-972e-4c18-bdbd-fd4fe268b339",
   "metadata": {},
   "outputs": [],
   "source": [
    "buildings = sd.read_parquet(\n",
    "    \"https://github.com/geoarrow/geoarrow-data/releases/download/v0.2.0/microsoft-buildings_point_geo.parquet\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "22baf082-736e-4881-9704-d57eea07068c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "┌─────────────────────────────────┐\n",
      "│             geometry            │\n",
      "│             geometry            │\n",
      "╞═════════════════════════════════╡\n",
      "│ POINT(-97.16154292 26.08759861) │\n",
      "├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤\n",
      "│ POINT(-97.1606625 26.08481)     │\n",
      "├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤\n",
      "│ POINT(-97.16133375 26.08519809) │\n",
      "└─────────────────────────────────┘\n"
     ]
    }
   ],
   "source": [
    "buildings.show(3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "e1d2d89d-8227-43ea-8507-fd6524fe2ac5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "SedonaSchema with 1 field:\n",
       "  geometry: geometry<WkbView(ogc:crs84)>"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "buildings.schema"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "e959081e-3a8d-4041-b00f-a19bca10be39",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "129735970"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "buildings.count()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "d9ef702f-e424-4e53-9629-da6ed256ee7f",
   "metadata": {},
   "outputs": [],
   "source": [
    "buildings.to_view(\"buildings\", overwrite=True)\n",
    "vermont.to_view(\"vermont\", overwrite=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "0c300dad-aa60-4291-a10b-d2a332b37593",
   "metadata": {},
   "outputs": [
    {
     "ename": "SedonaError",
     "evalue": "type_coercion\ncaused by\nError during planning: Mismatched CRS arguments: ogc:crs84 vs epsg:32618\nUse ST_Transform() or ST_SetSRID() to ensure arguments are compatible.",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mSedonaError\u001b[39m                               Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[20]\u001b[39m\u001b[32m, line 8\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;66;03m# Again, SedonaDB prevents accidentally joining files with mismatched CRSs.\u001b[39;00m\n\u001b[32m      2\u001b[39m \u001b[43msd\u001b[49m\u001b[43m.\u001b[49m\u001b[43msql\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\"\"\u001b[39;49m\n\u001b[32m      3\u001b[39m \u001b[33;43mSELECT count(*) from buildings\u001b[39;49m\n\u001b[32m      4\u001b[39m \u001b[33;43mJOIN vermont\u001b[39;49m\n\u001b[32m      5\u001b[39m \u001b[33;43mWHERE ST_Intersects(\u001b[39;49m\n\u001b[32m      6\u001b[39m \u001b[33;43m       buildings.geometry,\u001b[39;49m\n\u001b[32m      7\u001b[39m \u001b[33;43m       vermont.geometry)\u001b[39;49m\n\u001b[32m----> \u001b[39m\u001b[32m8\u001b[39m \u001b[33;43m\"\"\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mshow\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n",
      "\u001b[36mFile \u001b[39m\u001b[32m~/new-sedonadb/sedona-db/python/sedonadb/python/sedonadb/dataframe.py:380\u001b[39m, in \u001b[36mDataFrame.show\u001b[39m\u001b[34m(self, limit, width, ascii)\u001b[39m\n\u001b[32m    356\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Print the first limit rows to the console\u001b[39;00m\n\u001b[32m    357\u001b[39m \n\u001b[32m    358\u001b[39m \u001b[33;03mArgs:\u001b[39;00m\n\u001b[32m   (...)\u001b[39m\u001b[32m    377\u001b[39m \n\u001b[32m    378\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m    379\u001b[39m width = \u001b[38;5;28mself\u001b[39m._out_width(width)\n\u001b[32m--> \u001b[39m\u001b[32m380\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_impl\u001b[49m\u001b[43m.\u001b[49m\u001b[43mshow\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_ctx\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlimit\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mwidth\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mascii\u001b[49m\u001b[43m)\u001b[49m, end=\u001b[33m\"\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mSedonaError\u001b[39m: type_coercion\ncaused by\nError during planning: Mismatched CRS arguments: ogc:crs84 vs epsg:32618\nUse ST_Transform() or ST_SetSRID() to ensure arguments are compatible."
     ]
    }
   ],
   "source": [
    "# Again, SedonaDB prevents accidentally joining files with mismatched CRSs.\n",
    "sd.sql(\"\"\"\n",
    "SELECT count(*) from buildings\n",
    "JOIN vermont\n",
    "WHERE ST_Intersects(\n",
    "       buildings.geometry,\n",
    "       vermont.geometry)\n",
    "\"\"\").show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "648984ce-ac7a-4f76-ac9f-17bd5c628bd0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "┌──────────┐\n",
      "│ count(*) │\n",
      "│   int64  │\n",
      "╞══════════╡\n",
      "│   361856 │\n",
      "└──────────┘\n"
     ]
    }
   ],
   "source": [
    "sd.sql(\"\"\"\n",
    "SELECT count(*)\n",
    "FROM buildings\n",
    "JOIN vermont\n",
    "WHERE ST_Intersects(\n",
    "    buildings.geometry,\n",
    "    ST_Transform(vermont.geometry, 'EPSG:4326')\n",
    ")\n",
    "\"\"\").show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv (3.13.3)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
